package com.gwtintegration.smartrf;

import java.io.IOException;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.persistence.Entity;

import com.google.web.bindery.requestfactory.shared.EntityProxy;
import com.google.web.bindery.requestfactory.shared.ProxyFor;
import com.gwtintegration.smartrf.server.DataResponse;
import com.gwtintegration.smartrf.server.entity.Identifiable;
import com.gwtintegration.smartrf.server.entity.locator.JpaEntityLocator;
import com.gwtintegration.smartrf.shared.DataResponseValueProxy;
import com.gwtintegration.smartrf.util.AnnotationProcessorCodeWriter;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPackage;

@SupportedAnnotationTypes("javax.persistence.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class EntityProcessor extends AbstractProcessor {

	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

		try {
			JCodeModel codeModel = new JCodeModel();

			for (Element element : roundEnv.getElementsAnnotatedWith(Entity.class)) {
				if (element.getKind() == ElementKind.CLASS) {

					TypeElement entityType = (TypeElement) element;
					DeclaredType identifiableType = getInterface(entityType, Identifiable.class);

					if (identifiableType == null) {
						continue;
					}

					List<? extends TypeMirror> typeParameters = identifiableType.getTypeArguments();
					if (typeParameters == null || typeParameters.isEmpty()) {
						continue;
					}

					TypeMirror entityIdParameter = typeParameters.get(0);
					if (entityIdParameter.getKind() != TypeKind.DECLARED) {
						continue;
					}

					Element entityIdElement = processingEnv.getTypeUtils().asElement(entityIdParameter);
					if (entityIdElement.getKind() != ElementKind.CLASS) {
						continue;
					}

					TypeElement enetityIdType = (TypeElement) entityIdElement;

					PackageElement packageElement = (PackageElement) entityType.getEnclosingElement();
					String basePackageName = getBasePackageName(packageElement);

					JDefinedClass locatorClass = createEntityLocator(codeModel, entityType, enetityIdType, basePackageName);
					JDefinedClass proxyInterface = createEntityProxy(codeModel, entityType, basePackageName, locatorClass);
					JDefinedClass dataResponseClass = createEntityDataResponse(codeModel, entityType, basePackageName);
					JDefinedClass dataResponseProxyInterface = createEntityDataResponseProxy(codeModel, entityType, basePackageName, proxyInterface,
							dataResponseClass);
				}
			}

			AnnotationProcessorCodeWriter codeWriter = new AnnotationProcessorCodeWriter(processingEnv);
			codeModel.build(codeWriter);

		} catch (IOException e) {
			throw new RuntimeException("Unable to create a proxies for entities");
		}

		return true;
	}

	private JDefinedClass createEntityDataResponseProxy(JCodeModel codeModel, TypeElement entityType, String basePackageName,
			JDefinedClass proxyInterface, JDefinedClass dataResponseClass) {
		try {
			String packageName = basePackageName + ".shared.proxy.response";
			String interfaceName = entityType.getSimpleName().toString();

			if (interfaceName.endsWith("Entity")) {
				interfaceName = interfaceName.substring(0, interfaceName.indexOf("Entity"));
			}

			interfaceName += "DataResponseValueProxy";

			JPackage dataResponseProxyPackage = codeModel._package(packageName);
			JDefinedClass dataResponseProxyInterface = dataResponseProxyPackage._interface(interfaceName);

			JAnnotationUse proxyForAnnotation = dataResponseProxyInterface.annotate(ProxyFor.class);
			proxyForAnnotation.param("value", dataResponseClass);

			dataResponseProxyInterface._extends(codeModel.ref(DataResponseValueProxy.class).narrow(proxyInterface));

			dataResponseProxyInterface.method(JMod.NONE, Integer.class, "getTotalRows").annotate(Override.class);

			JMethod setTotalRowsMethods = dataResponseProxyInterface.method(JMod.NONE, codeModel.VOID, "setTotalRows");
			setTotalRowsMethods.annotate(Override.class);
			setTotalRowsMethods.param(Integer.class, "totalRows");

			dataResponseProxyInterface.method(JMod.NONE, codeModel.ref(List.class).narrow(proxyInterface), "getContent").annotate(Override.class);

			JMethod setContentMethod = dataResponseProxyInterface.method(JMod.NONE, codeModel.VOID, "setContent");
			setContentMethod.annotate(Override.class);
			setContentMethod.param(codeModel.ref(List.class).narrow(proxyInterface), "content");

			return dataResponseProxyInterface;

		} catch (Exception e) {
			throw new RuntimeException("Unable to create a proxy for " + entityType.getQualifiedName());
		}
	}

	private JDefinedClass createEntityDataResponse(JCodeModel codeModel, TypeElement entityType, String basePackageName) {
		try {
			String packageName = basePackageName + ".server.entity.response";
			String className = entityType.getSimpleName().toString();

			if (className.endsWith("Entity")) {
				className = className.substring(0, className.indexOf("Entity"));
			}

			className += "DataResponse";

			JPackage dataResponsePackage = codeModel._package(packageName);
			JDefinedClass dataResponseClass = dataResponsePackage._class(className);

			dataResponseClass._extends(codeModel.ref(DataResponse.class).narrow(codeModel.ref(entityType.getQualifiedName().toString())));

			return dataResponseClass;

		} catch (JClassAlreadyExistsException e) {
			throw new RuntimeException("Unable to create a DataResponse for " + entityType.getQualifiedName());
		}
	}

	private String getBasePackageName(PackageElement packageElement) {
		String basePackageName = packageElement.getQualifiedName().toString();

		if (basePackageName != null) {
			int index = basePackageName.indexOf(".server.");

			if (index != -1) {
				basePackageName = basePackageName.substring(0, index);
			}
		}
		return basePackageName;
	}

	private JDefinedClass createEntityLocator(JCodeModel codeModel, TypeElement entityType, TypeElement entityIdType, String basePackageName) {
		try {
			String packageName = basePackageName + ".server.entity.locator";
			String className = entityType.getSimpleName() + "Locator";

			JPackage locatorPackage = codeModel._package(packageName);
			JDefinedClass locatorClass = locatorPackage._class(className);

			locatorClass._extends(codeModel.ref(JpaEntityLocator.class).narrow(codeModel.ref(entityType.getQualifiedName().toString()),
					codeModel.ref(entityIdType.getQualifiedName().toString())));

			return locatorClass;

		} catch (JClassAlreadyExistsException e) {
			throw new RuntimeException("Unable to create a locator for " + entityType.getQualifiedName());
		}
	}

	private JDefinedClass createEntityProxy(JCodeModel codeModel, TypeElement entityType, String basePackageName, JDefinedClass locatorClass) {
		try {
			String packageName = basePackageName + ".shared.proxy";
			String className = getEntityProxyClassName(entityType);

			JPackage entityProxyPackage = codeModel._package(packageName);
			JDefinedClass entityProxyInterface = entityProxyPackage._interface(className);
			entityProxyInterface._extends(EntityProxy.class);

			JAnnotationUse proxyForAnnotation = entityProxyInterface.annotate(ProxyFor.class);
			proxyForAnnotation.param("value", codeModel.ref(entityType.getQualifiedName().toString()));
			proxyForAnnotation.param("locator", locatorClass);

			for (ExecutableElement method : ElementFilter.methodsIn(entityType.getEnclosedElements())) {

				String methodName = method.getSimpleName().toString();

				if (methodName.startsWith("get") || methodName.startsWith("is")) {

					TypeMirror returnTypeMirror = method.getReturnType();

					if (returnTypeMirror.getKind() != TypeKind.DECLARED) {
						continue;
					}

					// DeclaredType returnDeclaredType = (DeclaredType) returnTypeMirror;
					Element returnElement = processingEnv.getTypeUtils().asElement(returnTypeMirror);
					if (returnElement.getKind() != ElementKind.CLASS) {
						continue;
					}

					TypeElement returnType = (TypeElement) returnElement;

					if (returnType.getAnnotation(Entity.class) != null) {
						String subClassName = packageName + "." + getEntityProxyClassName(returnType);
						entityProxyInterface.method(JMod.NONE, codeModel.ref(subClassName), methodName);
					} else {
						entityProxyInterface.method(JMod.NONE, codeModel.ref(returnType.getQualifiedName().toString()), methodName);
					}
				} else if (methodName.startsWith("set")) {

					List<? extends VariableElement> parameters = method.getParameters();
					if (parameters == null || parameters.size() != 1) {
						continue;
					}

					VariableElement variableElement = parameters.get(0);
					TypeMirror varTypeMirror = variableElement.asType();
					if (varTypeMirror.getKind() != TypeKind.DECLARED) {
						continue;
					}

					Element varElement = processingEnv.getTypeUtils().asElement(varTypeMirror);
					if (varElement.getKind() != ElementKind.CLASS) {
						continue;
					}

					TypeElement varType = (TypeElement) varElement;

					if (varType.getAnnotation(Entity.class) != null) {
						String subClassName = packageName + "." + getEntityProxyClassName(varType);
						JMethod setMethod = entityProxyInterface.method(JMod.NONE, codeModel.VOID, methodName);
						setMethod.param(codeModel.ref(subClassName), variableElement.getSimpleName().toString());
					} else {
						JMethod setMethod = entityProxyInterface.method(JMod.NONE, codeModel.VOID, methodName);
						setMethod.param(codeModel.ref(varType.getQualifiedName().toString()), variableElement.getSimpleName().toString());
					}
				}
			}

			return entityProxyInterface;

		} catch (Exception e) {
			throw new RuntimeException("Unable to create a proxy for " + entityType.getQualifiedName());
		}
	}

	private String getEntityProxyClassName(TypeElement entityType) {
		String className = entityType.getSimpleName().toString();

		if (className.endsWith("Entity")) {
			className += "Proxy";
		} else {
			className += "EntityProxy";
		}

		return className;
	}

	protected DeclaredType getInterface(TypeElement typeElement, Class<?> interfaceClass) {

		for (TypeMirror typeMirror : typeElement.getInterfaces()) {

			if (typeMirror.getKind() == TypeKind.DECLARED) {

				DeclaredType declaredType = (DeclaredType) typeMirror;

				Element element = processingEnv.getTypeUtils().asElement(typeMirror);

				if (element.getKind() == ElementKind.INTERFACE) {
					TypeElement interfaceElement = (TypeElement) element;

					if (interfaceElement.getQualifiedName().toString().equals(interfaceClass.getName())) {
						return declaredType;
					}
				}
			}
		}

		return null;
	}
}
